spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { EventEvidence } from '@/components/events/event-evidence';5import { EventList } from '@/components/events/event-row';6import { CountryChip, EventTypeBadge, ImportanceMeter, SignificanceBadge } from '@/components/ui/badges';7import { LiveAgo } from '@/components/ui/live';8import { Container, Note } from '@/components/ui/section';9import { api, ApiError, safe } from '@/lib/api';10import { fmtDateTime, fmtInt } from '@/lib/format';11import { routes, SITE_NAME } from '@/lib/site';12import type { EventDetail } from '@/lib/types';1314export const revalidate = 120;1516async function load(id: string): Promise<EventDetail> {17 try {18 return await api.event(id);19 } catch (e) {20 if (e instanceof ApiError && e.notFound) notFound();21 throw e;22 }23}2425export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {26 const { id } = await params;27 const e = await safe(api.event(id));28 if (!e) return { title: 'Event' };29 return { title: e.title, description: e.summary ?? `${e.event_type} event detected for ${e.company.display_name} on ${fmtDateTime(e.detected_at)}.`, alternates: { canonical: `/events/${e.id}` }, openGraph: { title: `${e.title} | ${SITE_NAME}`, description: e.summary ?? undefined, type: 'article', publishedTime: e.detected_at }, robots: e.status === 'active' ? undefined : { index: false } };30}3132export default async function EventPage({ params }: { params: Promise<{ id: string }> }) {33 const { id } = await params;34 const e = await load(id);35 const related = await safe(api.companyEvents(e.company.slug, { per_page: 6 }));36 const others = (related?.items ?? []).filter((x) => x.id !== e.id).slice(0, 5);37 const jsonLd = { '@context': 'https://schema.org', '@type': 'NewsArticle', headline: e.title, datePublished: e.detected_at, dateModified: e.detected_at, about: { '@type': 'Organization', name: e.company.display_name, url: `https://${e.company.canonical_domain}` }, isBasedOn: e.source_url ?? undefined, publisher: { '@type': 'Organization', name: SITE_NAME } };38 return (39 <Container>40 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />41 <div className="pb-4 pt-6 md:pt-10">42 <p className="eyebrow flex flex-wrap items-center gap-2">43 <Link href={routes.events()} className="hover:text-ink">44 Events45 </Link>46 <span>/</span>47 <EventTypeBadge type={e.event_type} subtype={e.event_subtype} />48 {e.status !== 'active' && <span className="text-danger">{e.status}</span>}49 </p>50 <h1 className={`display mt-2 text-[26px] md:text-[36px] ${e.status === 'retracted' ? 'line-through decoration-danger/60' : ''}`}>{e.title}</h1>51 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-sm text-ink-2">52 <Link href={routes.company(e.company.slug)} className="font-medium text-ink hover:text-accent">53 {e.company.display_name}54 </Link>55 <span className="mono text-xs text-ink-3">{e.company.canonical_domain}</span>56 <CountryChip code={e.company.country} />57 <span className="inline-flex items-center gap-1.5 text-xs text-ink-3">58 <ImportanceMeter importance={e.importance} /> detected <LiveAgo at={e.detected_at} tick={30000} />59 </span>60 </div>61 {e.summary && <p className="mt-4 max-w-3xl text-[16px] leading-relaxed text-ink-2">{e.summary}</p>}62 </div>63 <div className="grid gap-8 lg:grid-cols-12">64 <div className="lg:col-span-8">65 <p className="eyebrow mb-2">Evidence</p>66 <EventEvidence event={e} sources={e.sources} />67 {e.change && (68 <section className="mt-6">69 <p className="eyebrow mb-2">Underlying change</p>70 <div className="flex flex-wrap items-center gap-3 border border-rule p-3 text-sm">71 <SignificanceBadge value={e.change.significance} />72 <span className="tnum text-ink-2">73 +{fmtInt(e.change.blocks_added)} / −{fmtInt(e.change.blocks_removed)} / ~{fmtInt(e.change.blocks_modified)} blocks74 </span>75 <span className="text-xs text-ink-3">on {e.change.surface} · {fmtDateTime(e.change.detected_at)}</span>76 <Link href={routes.change(e.change.id)} className="link ml-auto text-xs">77 Open block-level diff →78 </Link>79 </div>80 </section>81 )}82 {Object.keys(e.payload ?? {}).length > 0 && (83 <details className="mt-6">84 <summary className="cursor-pointer text-sm text-ink-2 hover:text-ink">Structured payload & entities</summary>85 <pre className="mt-2 overflow-x-auto border border-rule bg-surface-2 p-3 text-xs">{JSON.stringify({ payload: e.payload, entities: e.entities }, null, 2)}</pre>86 </details>87 )}88 </div>89 <aside className="lg:col-span-4">90 <p className="eyebrow mb-2">More from {e.company.display_name}</p>91 <EventList events={others} variant="table" showCompany={false} emptyLabel="No other events yet." />92 <p className="mt-2 text-sm">93 <Link href={routes.company(e.company.slug, 'timeline')} className="link">94 Company timeline →95 </Link>96 </p>97 <Note className="mt-6">This event is an interpretation of an observed change on a public web page. It links to the page as it was when detected; if the page has since been removed, the observation is preserved in the snapshot archive.</Note>98 </aside>99 </div>100 </Container>101 );102}103